#!/usr/bin/php -q
<?PHP
/* Copyright 2005-2023, Lime Technology
 * Copyright 2012-2023, Bergware International.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation.
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 */
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
extract(parse_plugin_cfg('dynamix',true));

// add translations
$_SERVER['REQUEST_URI'] = '';
$login_locale = _var($display,'locale');
require_once "$docroot/plugins/dynamix.docker.manager/include/DockerClient.php";

$var = parse_ini_file('/var/local/emhttp/var.ini');
$DockerClient = new DockerClient();
$DockerUpdate = new DockerUpdate();
$DockerTemplates = new DockerTemplates();

$custom = DockerUtil::custom();
$subnet = DockerUtil::network($custom);
$cpus   = DockerUtil::cpus();

function write(...$messages){
  $com = curl_init();
  curl_setopt_array($com,[
    CURLOPT_URL => 'http://localhost/pub/docker?buffer_length=1',
    CURLOPT_UNIX_SOCKET_PATH => '/var/run/nginx.socket',
    CURLOPT_POST => 1,
    CURLOPT_RETURNTRANSFER => true
  ]);
  foreach ($messages as $message) {
    curl_setopt($com, CURLOPT_POSTFIELDS, $message);
    curl_exec($com);
  }
  curl_close($com);
}
function stopContainer_nchan($name) {
  global $DockerClient;
  $waitID = mt_rand();
  write("<p class='logLine'></p>","addLog\0<fieldset class='docker'><legend>"._('Stopping container').": ".htmlspecialchars($name)."</legend><p class='logLine'></p><span id='wait-$waitID'>"._('Please wait')." </span></fieldset>","show_Wait\0$waitID");
  $retval = $DockerClient->stopContainer($name);
  $out = ($retval === true) ? _('Successfully stopped container').": $name" : _('Error').": ".$retval;
  write("stop_Wait\0$waitID","addLog\0<b>".htmlspecialchars($out)."</b>");
}
function removeContainer_nchan($name) {
  global $DockerClient;
  $waitID = mt_rand();
  write("<p class='logLine'></p>","addLog\0<fieldset class='docker'><legend>"._('Removing container').": ".htmlspecialchars($name)."</legend><p class='logLine'></p><span id='wait-$waitID'>"._('Please wait')." </span></fieldset>","show_Wait\0$waitID");
  $retval = $DockerClient->removeContainer($name);
  $out = ($retval === true) ? _('Successfully removed container').": $name" : _('Error').": ".$retval;
  write("stop_Wait\0$waitID","addLog\0<b>".htmlspecialchars($out)."</b>");
}
function removeImage_nchan($image) {
  global $DockerClient;
  $waitID = mt_rand();
  write("<p class='logLine'></p>","addLog\0<fieldset class='docker'><legend>"._('Removing orphan image').": ".htmlspecialchars($image)."</legend><p class='logLine'></p><span id='wait-$waitID'>"._('Please wait')." </span></fieldset>","show_Wait\0$waitID");
  $retval = $DockerClient->removeImage($image);
  $out = ($retval === true) ? _('Successfully removed orphan image').": $image" : _('Error').": ".$retval;
  write("stop_Wait\0$waitID","addLog\0<b>".htmlspecialchars($out)."</b>");
}
function pullImage_nchan($name, $image) {
  global $DockerClient, $DockerTemplates, $DockerUpdate;
  $waitID = mt_rand();
  if (!preg_match("/:\S+$/", $image)) $image .= ":latest";
  write("<p class='logLine'></p>","addLog\0<fieldset class='docker'><legend>"._('Pulling image').": ".htmlspecialchars($image)."</legend><p class='logLine'></p><span id='wait-$waitID'>"._('Please wait')." </span></fieldset>","show_Wait\0$waitID");
  $alltotals = [];
  $laststatus = [];
  $strError = '';
  $DockerClient->pullImage($image, function ($line) use (&$alltotals, &$laststatus, &$waitID, &$strError, $image, $DockerClient, $DockerUpdate) {
    $cnt = json_decode($line, true);
    $id = $cnt['id'] ?? '';
    $status = $cnt['status'] ?? '';
    if (isset($cnt['error'])) $strError = $cnt['error'];
    if ($waitID !== false) {
      write("stop_Wait\0$waitID");
      $waitID = false;
    }
    if (empty($status)) return;
    if (!empty($id)) {
      if (!empty($cnt['progressDetail']) && !empty($cnt['progressDetail']['total'])) {
        $alltotals[$id] = $cnt['progressDetail']['total'];
      }
      if (empty($laststatus[$id])) {
        $laststatus[$id] = '';
      }
      switch ($status) {
      case 'Waiting':
        // Omit
        break;
      case 'Downloading':
        if ($laststatus[$id] != $status) {
          write("addToID\0$id\0".htmlspecialchars($status));
        }
        $total = $cnt['progressDetail']['total'];
        $current = $cnt['progressDetail']['current'];
        if ($total > 0) {
          $percentage = round(($current / $total) * 100);
          write("progress\0$id\0 ".$percentage."% "._('of')." ".$DockerClient->formatBytes($total));
        } else {
          // Docker must not know the total download size (http-chunked or something?)
          // just show the current download progress without the percentage
          $alltotals[$id] = $current;
          write("progress\0$id\0".$DockerClient->formatBytes($current));
        }
        break;
      default:
        if ($laststatus[$id] == "Downloading") {
          write("progress\0$id\0 100% "._('of')." ".$DockerClient->formatBytes($alltotals[$id]));
        }
        if ($laststatus[$id] != $status) {
          write("addToID\0".($id=='latest'?mt_rand():$id)."\0".htmlspecialchars($status));
        }
        break;
      }
      $laststatus[$id] = $status;
    } else {
      if (strpos($status, 'Status: ') === 0) {
        write("addLog\0".htmlspecialchars($status));
      }
      if (strpos($status, 'Digest: ') === 0) {
        $DockerUpdate->setUpdateStatus($image, substr($status,8));
      }
    }
  });
  write("addLog\0<br><b>"._('TOTAL DATA PULLED').":</b> ".$DockerClient->formatBytes(array_sum($alltotals)));
  if (!empty($strError)) {
    write("addLog\0<br><span class='error'><b>"._('Error').":</b> ".htmlspecialchars($strError)."</span>");
    return false;
  }
  return true;
}
function execCommand_nchan($command) {
  $waitID = mt_rand();
  [$cmd,$args] = explode(' ',$command,2);
  write("<p class='logLine'></p>","addLog\0<fieldset class='docker'><legend>"._('Command execution')."</legend>".basename($cmd).' '.str_replace(" -","<br>&nbsp;&nbsp;-",htmlspecialchars($args))."<br><span id='wait-$waitID'>"._('Please wait')." </span><p class='logLine'></p></fieldset>","show_Wait\0$waitID");
  $proc = popen("$command 2>&1",'r');
  while ($out = fgets($proc)) {
    $out = preg_replace("%[\t\n\x0B\f\r]+%", '',$out);
    write("addLog\0".htmlspecialchars($out));
  }
  $retval = pclose($proc);
  $out = $retval ? _('The command failed').'.' : _('The command finished successfully').'!';
  write("stop_Wait\0$waitID","addLog\0<br><b>$out</b>");
  return $retval===0;
}

$style = ["<style>"];
$style[] = ".logLine{font-family:bitstream!important;font-size:1.2rem!important;margin:0;padding:0}";
$style[] = "fieldset.docker{border:solid thin;margin-top:8px}";
$style[] = "legend{font-size:1.1rem!important;font-weight:bold}";
$style[] = "</style>";

write(implode($style)."<p class='logLine'></p>");
foreach (explode('*',rawurldecode($argv[1])) as $value) {
  $tmpl = $DockerTemplates->getUserTemplate($value);
  if (!$tmpl) {
    write("addLog\0"._('Configuration not found').". "._('Was this container created using this plugin')."?");
    continue;
  }
  $xml = file_get_contents($tmpl);
  [$cmd, $Name, $Repository] = xmlToCommand($tmpl);
  $Registry = getXmlVal($xml, "Registry");
  $TS_Enabled = getXmlVal($xml, "TailscaleEnabled");
  $oldImageID = $DockerClient->getImageID($Repository);
  // pull image
  if (!pullImage_nchan($Name, $Repository)) continue;
  $oldContainerInfo = $DockerClient->getContainerDetails($Name);
  // determine if the container is still running
  $startContainer = false;
  if (!empty($oldContainerInfo) && !empty($oldContainerInfo['State']) && !empty($oldContainerInfo['State']['Running'])) {
    // since container was already running, put it back it to a running state after update
    $startContainer = true;
    // attempt graceful stop of container first
    stopContainer_nchan($Name);
  }
  if ( ($argv[2]??null) == "ca_docker_run_override" )
    $startContainer = true;
  if ( $startContainer )
    $cmd = str_replace('/docker create ', '/docker run -d ', $cmd);
  // force kill container if still running after 10 seconds
  if (empty($_GET['communityApplications'])) removeContainer_nchan($Name);
  // Extract real Entrypoint and Cmd from container for Tailscale
  if ($TS_Enabled == 'true') {
    // Create preliminary base container but don't run it
    exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker create --name '" . escapeshellarg($Name) . "' '" . escapeshellarg($Repository) . "'");
    // Get Entrypoint and Cmd from docker inspect
    $containerInfo = $DockerClient->getContainerDetails($Name);
    $ts_env  = isset($containerInfo['Config']['Entrypoint']) ? '-e ORG_ENTRYPOINT="' . implode(' ', $containerInfo['Config']['Entrypoint']) . '" ' : '';
    $ts_env .= isset($containerInfo['Config']['Cmd']) ? '-e ORG_CMD="' . implode(' ', $containerInfo['Config']['Cmd']) . '" ' : '';
    // Insert Entrypoint and Cmd to docker command
    $cmd = str_replace('-l net.unraid.docker.managed=dockerman', $ts_env . '-l net.unraid.docker.managed=dockerman' , $cmd);
    // Remove preliminary container
    exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker rm '" . escapeshellarg($Name) . "'");
  }
  execCommand_nchan($cmd);
  if ($startContainer) addRoute($Name); // add route for remote WireGuard access
  $DockerClient->flushCaches();
  $newImageID = $DockerClient->getImageID($Repository);
  // remove old orphan image since it's no longer used by this container
  if ($oldImageID && $oldImageID != $newImageID) removeImage_nchan($oldImageID);
}
write('_DONE_','');
?>
